﻿//------------------------------------------------------------------------------
// <copyright file="MonitorDocumentsService.cs" company="Microsoft">
//     Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
//------------------------------------------------------------------------------

using System;
using System.Collections.Generic;
using System.Text;
using System.Diagnostics;
using System.Runtime.InteropServices;
using Microsoft.VisualStudio;
using VsShell = Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.Modeling.Shell;

namespace ModelingAddIns
{
    #region IMonitorDocumentsService interface

    /// <summary>
    /// Monitors running documents table and issues events when documents are opened or closed.
    /// </summary>
    //[CLSCompliant(false)]
    public interface IMonitorDocumentsService
    {
        /// <summary>
        /// Raised just before a new document is about to show.
        /// </summary>
        event EventHandler<DocumentEventArgs> DocumentOpened;

        /// <summary>
        /// Raised after document has been closed.
        /// </summary>
        event EventHandler<DocumentEventArgs> DocumentClosed;

        /// <summary>
        /// Runs specified action for all opened documents in the running document table.
        /// </summary>
        /// <param name="action">Action taking DocData object as a single parameter.</param>
        void ForAllOpenedDocuments(Action<DocumentEventArgs> action);
    }

    /// <summary>
    /// Arguments for document events.
    /// </summary>
    public class DocumentEventArgs : EventArgs
    {
        private VsShell.IVsRunningDocumentTable m_rdt;
        private uint m_docCookie;
        private object m_docData;

        //[CLSCompliant(false)]
        public DocumentEventArgs(VsShell.IVsRunningDocumentTable rdt, uint docCookie)
        {
            if (rdt == null) throw new ArgumentNullException("rdt");
            if (docCookie <= 0) throw new ArgumentOutOfRangeException("docCookie");

            m_rdt = rdt;
            m_docCookie = docCookie;
        }

        //[CLSCompliant(false)]
        public uint DocCookie
        {
            [DebuggerStepThrough]
            get { return m_docCookie; }
        }

        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        public object DocData
        {
            [DebuggerStepThrough]
            get
            {
                if (m_docData == null)
                {
                    uint rdtFlags, readLocks, editLocks, itemID;
                    string itemName;
                    VsShell.IVsHierarchy hierarchy;
                    IntPtr punkDocData = IntPtr.Zero;
                    try
                    {
                        if (ErrorHandler.Succeeded(m_rdt.GetDocumentInfo(m_docCookie, out rdtFlags, out readLocks, out editLocks, out itemName, out hierarchy, out itemID, out punkDocData)))
                        {
                            m_docData = Marshal.GetObjectForIUnknown(punkDocData);
                        }
                    }
                    finally
                    {
                        if (punkDocData != IntPtr.Zero)
                            Marshal.Release(punkDocData);
                    }
                }
                return m_docData;
            }
        }
    }

    #endregion

    /// <summary>
    /// Default implementation of the IMonitorDocumentsService interface.
    /// </summary>
    internal class MonitorDocumentsService : IMonitorDocumentsService, IDisposable, VsShell.IVsRunningDocTableEvents
    {
        #region Private data, constructor

        private VsShell.IVsRunningDocumentTable m_rdt;
        private uint m_rdtEventsCookie;
        private List<uint> m_openedDocuments = new List<uint>();

        internal MonitorDocumentsService(IServiceProvider serviceProvider)
        {
            if (serviceProvider == null) throw new ArgumentNullException("serviceProvider");

            m_rdt = serviceProvider.GetService(typeof(VsShell.IVsRunningDocumentTable)) as VsShell.IVsRunningDocumentTable;
            Debug.Assert(m_rdt != null);
            if (m_rdt != null)
            {
                ErrorHandler.ThrowOnFailure(m_rdt.AdviseRunningDocTableEvents(this, out m_rdtEventsCookie));
            }
        }

        #endregion

        #region IDisposable implementation

        ~MonitorDocumentsService()
        {
            Dispose(false);
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
        protected virtual void Dispose(bool disposing)
        {
            Debug.Assert(disposing, "Finalizing MonitorDocumentService without disposing!");
            Exception disposeException = null;
            if (disposing)
            {
                if (m_openedDocuments != null)
                {
                    if (DocumentClosed != null)
                    {
                        foreach (uint docCookie in m_openedDocuments)
                        {
                            try
                            {
                                DocumentClosed(this, new DocumentEventArgs(m_rdt, docCookie));
                            }
                            catch (Exception ex)
                            {
                                disposeException = ex;
                            }
                        }
                    }
                    m_openedDocuments = null;
                }
                if (m_rdt != null)
                {
                    if (m_rdtEventsCookie > 0)
                    {
                        ErrorHandler.ThrowOnFailure(m_rdt.UnadviseRunningDocTableEvents(m_rdtEventsCookie));
                        m_rdtEventsCookie = 0;
                    }
                    m_rdt = null;
                }
            }
            if (disposeException != null)
                throw disposeException;
        }

        #endregion

        #region VsShell.IVsRunningDocTableEvents Members

        public int OnAfterAttributeChange(uint docCookie, uint grfAttribs)
        {
            return VSConstants.S_OK;
        }

        public int OnAfterDocumentWindowHide(uint docCookie, VsShell.IVsWindowFrame pFrame)
        {
            return VSConstants.S_OK;
        }

        public int OnAfterFirstDocumentLock(uint docCookie, uint dwRDTLockType, uint dwReadLocksRemaining, uint dwEditLocksRemaining)
        {
            return VSConstants.S_OK;
        }

        public int OnAfterSave(uint docCookie)
        {
            return VSConstants.S_OK;
        }

        public int OnBeforeDocumentWindowShow(uint docCookie, int fFirstShow, VsShell.IVsWindowFrame pFrame)
        {
            Debug.Assert(pFrame != null);
            if (pFrame != null && fFirstShow != 0)
            {
                if (!m_openedDocuments.Contains(docCookie))
                {
                    m_openedDocuments.Add(docCookie);

                    EventHandler<DocumentEventArgs> eventHandler = this.DocumentOpened;
                    if (eventHandler != null)
                        eventHandler(this, new DocumentEventArgs(m_rdt, docCookie));
                }
            }
            return VSConstants.S_OK;
        }

        public int OnBeforeLastDocumentUnlock(uint docCookie, uint dwRDTLockType, uint dwReadLocksRemaining, uint dwEditLocksRemaining)
        {
            if (dwReadLocksRemaining + dwEditLocksRemaining < 1)
            {
                if (m_openedDocuments.Contains(docCookie))
                {
                    m_openedDocuments.Remove(docCookie);

                    EventHandler<DocumentEventArgs> eventHandler = this.DocumentClosed;
                    if (eventHandler != null)
                        eventHandler(this, new DocumentEventArgs(m_rdt, docCookie));
                }
            }
            return VSConstants.S_OK;
        }

        #endregion

        #region IMonitorDocumentsService Members

        public event EventHandler<DocumentEventArgs> DocumentOpened;
        public event EventHandler<DocumentEventArgs> DocumentClosed;

        public void ForAllOpenedDocuments(Action<DocumentEventArgs> action)
        {
            if (action == null) throw new ArgumentNullException("action");

            if (m_rdt != null)
            {
                VsShell.IEnumRunningDocuments runningDocuments = null;
                ErrorHandler.ThrowOnFailure(m_rdt.GetRunningDocumentsEnum(out runningDocuments));
                Debug.Assert(runningDocuments != null);
                if (runningDocuments != null)
                {
                    ErrorHandler.ThrowOnFailure(runningDocuments.Reset());
                    uint[] cookie = new uint[1];
                    uint fetchedCount;
                    while (ErrorHandler.Succeeded(runningDocuments.Next(1, cookie, out fetchedCount)) && fetchedCount == 1 && cookie[0] != 0)
                    {
                        uint rdtFlags;
                        uint readLocks;
                        uint editLocks;
                        string pathName;
                        VsShell.IVsHierarchy hierarchy;
                        uint itemid;
                        IntPtr docData;
                        if (!m_openedDocuments.Contains(cookie[0]) && VSConstants.S_OK == m_rdt.GetDocumentInfo(cookie[0], out rdtFlags, out readLocks, out editLocks, out pathName, out hierarchy, out itemid, out docData))
                        {
                            if (readLocks + editLocks > 0)
                            {
                                m_openedDocuments.Add(cookie[0]);
                            }
                        }

                        action(new DocumentEventArgs(m_rdt, cookie[0]));
                    }
                }
            }
        }

        #endregion
    }
}